package org.longteng.security.service.impl;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.longteng.core.base.TokenResult;
import org.longteng.dao.model.OauthClient;
import org.longteng.security.Constants;
import org.longteng.security.jwtkeys.JwtECDSAKey;
import org.longteng.security.jwtkeys.JwtKeyPair;
import org.longteng.security.jwtkeys.JwtRSAKey;
import org.longteng.security.service.AuthUserService;
import org.longteng.security.service.JwtTokenService;
import org.longteng.security.utils.SecretKeyUtils;
import org.longteng.security.vo.AuthorizationUserVo;
import org.longteng.security.vo.AuthorizationVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.security.auth.message.AuthException;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * jwt密钥处理默认实现
 * @author hymn.com
 * @date 2022/12/07 10:00:00
 */
@Slf4j
@Service
//@Component
public class JwtTokenServiceImpl implements JwtTokenService {

    private static final String JWT_ISSUER = "ELINKBUS_CSGVPP_API";  // 发布者
    private JwtKeyPair jwtKey;  // 初始化取得后一直用这个公私钥对
    /**
     * jwt有效时间，默认30分钟，单位秒，默认时间，数据表有就从数据表来OauthClient.tokenValidity
     */
    private final int defaultTokenTimeout = 30*60;

    /**
     * 服务端签名密钥加密名配置加密参数
     */
    @Value("${app.token.sign-alg:ECDSA}")
    private String signAlg;

    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        log.info("#启动初始化。jwt密钥({})处理,启动时生成一对有效公私钥，放在redis中，多个服务共用一对.", signAlg);
        this.jwtKey = this.generatorKeys(signAlg);
    }
    @Autowired
    private AuthUserService authUserService;

    /**
     * 取得生成的公私钥
     * @return
     */
    private JwtKeyPair getTokenKey() {
        return this.jwtKey;
    }

    /**
     * 取得生成默认的公私钥
     * @return
     */
    private JwtKeyPair generatorDefaultKey() {
        return generatorKeys("RSA");
    }
    /**
     * 取得生成的公私钥，以生成token; 启动时生成一对有效公私钥，放在redis中，多个服务共用一对
     * @param signAlg 需要指定[RSA/ECDSA]
     *
     * @return
     */
    private JwtKeyPair generatorKeys(String signAlg) {
        String publicKeyBase64 = stringRedisTemplate.opsForValue().get(PUBLIC_KEY_PATH);
        String privateKeyBase64 = stringRedisTemplate.opsForValue().get(PRIVATE_KEY_PATH);
        if (StringUtils.isAnyBlank(publicKeyBase64, privateKeyBase64)) {
            log.info("redis无公/私钥，开始生成 及取得公/私钥对。");
            JwtKeyPair jwtKey = SecretKeyUtils.generaJwtKey(signAlg);
            try {
                if (jwtKey == null) {
                    throw new NoSuchAlgorithmException("加密算法无效，需要指定[RSA/ECDSA]。");
                }
                jwtKey.initBuildKey();

                publicKeyBase64 = jwtKey.getPublicKeyEncBASEStr();
                privateKeyBase64 = jwtKey.getPrivateKeyEncBASEStr();

                stringRedisTemplate.opsForValue().set(PUBLIC_KEY_PATH, publicKeyBase64);
                stringRedisTemplate.opsForValue().set(PRIVATE_KEY_PATH, privateKeyBase64);
            }catch (Exception e) {
                log.error("生成generatorKeys及取得公/私钥错误。",e);
                throw new RuntimeException("生成 及取得公/私钥错误.");
            }
            return jwtKey;
        } else {
            log.info("redis取得公/私钥对。");
        }

        return SecretKeyUtils.readJwtKeyPair(publicKeyBase64, privateKeyBase64, signAlg);
    }

    /**
     * 生成Jwt加密类
     * @param jwtKey 密钥类
     * @return
     */
    private Algorithm generatorAlgorithm(JwtKeyPair jwtKey) {
        if (jwtKey instanceof JwtECDSAKey) {
            JwtECDSAKey jwtECDSAKey = (JwtECDSAKey)jwtKey;
            return Algorithm.ECDSA256(
                    jwtECDSAKey.getPublicKey(), jwtECDSAKey.getPrivateKey());
        } else {
            log.warn("不是指定的ECDSA加密就用RAS加密算法.");
            Optional<JwtKeyPair> opJwtKey = Optional.of(jwtKey);
            JwtRSAKey jwtRSAKey = (JwtRSAKey)opJwtKey.orElseGet(this::generatorDefaultKey);
            return Algorithm.RSA512(
                    jwtRSAKey.getPublicKey(), jwtRSAKey.getPrivateKey());
        }
    }

    /**
     * 生成token
     * @param jwtAudience
     * @param authVo
     * @param appIdTokenTimeout 数据表配置的appid对应token过期时间（秒）,refreshToken时传入null用默认的时长
     * @return
     */
    private TokenResult.TokenData internalBuildToken(String[] jwtAudience, AuthorizationVo authVo
            , Integer appIdTokenTimeout) {

        Optional.ofNullable(authVo).orElseThrow(()->new RuntimeException("验证信息authVo为空,无法生成token"));

        String[] audience  = jwtAudience;
        Date issuedAtDate = new Date(); // 生成签名的开始时间
        log.info("开始生成token，接受者audience：{}", StringUtils.join(audience,","));
        int tokenTimeout = appIdTokenTimeout != null && appIdTokenTimeout.intValue()>0
                ? appIdTokenTimeout.intValue()
                : this.defaultTokenTimeout;
        Date expiresAtDate = DateUtils.addSeconds(issuedAtDate, tokenTimeout); // 生成签名的有效期
        if (log.isDebugEnabled()) {
            log.debug("jwt生成toke,AppId:{},生成签名的时间:{},tokenTimeout:{}, 有效期到:{} "
                    , authVo.getAppId(), issuedAtDate, tokenTimeout, issuedAtDate);
        }

        JwtKeyPair jwtKey = this.getTokenKey(); // 获取公钥/私钥
        String token = "";
        String pk = null;
        try {
            // 安全算法类型 Algorithm.ECDSA512(),  Algorithm.RSA256
            Algorithm algorithm = this.generatorAlgorithm(jwtKey);
            token = JWT.create()
                    .withIssuer(JWT_ISSUER)        //发布者
                    .withAudience(audience)         //观众，相当于接受者
                    .withIssuedAt(issuedAtDate)   // 生成签名的时间
                    .withExpiresAt(expiresAtDate)    // 生成签名的有效期
                    .withClaim(TOKEN_CLAIM_NAME_APPID, authVo.getAppId()) //存数据
                    .withClaim(TOKEN_CLAIM_NAME_SIGN, authVo.getSign()) //存数据
                    .withNotBefore(new Date())  //生效时间
                    .withJWTId(jwtKey.getKeyId())    //编号, UUID.randomUUID().toString()
                    .sign(algorithm);                            //签入
            log.info("生成token完成。");
            if (log.isDebugEnabled()) {
                log.debug("token：\n{}", token);
            }

            pk = jwtKey.getPublicKeyEncBASEStr();
        } catch (Exception e) {
            log.error("生成token错误，", e);
            throw new RuntimeException("生成token错误，", e);
        }
        return TokenResult.TokenData.buildData(token, pk);
    }

    /**
     * 生成token
     *
     * @param authUserVo
     * @return
     */
    @Override
    public TokenResult.TokenData buildToken(AuthorizationUserVo authUserVo) throws AuthException {
        // 1验证sign是否合法，2验证appid是否有效，3验证用户是否有效
        OauthClient authClient = authUserService.verifySignAuthUser(authUserVo);

        String[] audience  = {"app","web", authUserVo.getUser()};
        return this.internalBuildToken(audience, authUserVo, authClient.getTokenValidity());
    }

    /**
     * 验证token
     *
     * @param token
     * @param authVo
     */
    @Override
    public void verify(String token, AuthorizationVo authVo) {
        JwtKeyPair jwtKey = this.getTokenKey(); // 获取公钥/私钥
        try {
            Optional.ofNullable(authVo).orElseThrow(()->new Exception("验证信息为空"));

            // 其实按照规定只需要传递 publicKey 来校验即可, Algorithm.RSA256
            Algorithm algorithm = this.generatorAlgorithm(jwtKey);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(JWT_ISSUER)
                    .build(); // Reusable verifier instance 可复用的验证实例
            DecodedJWT jwt = verifier.verify(token);
            Map<String, Claim> map = jwt.getClaims();
            Claim appIdClaim = map.get(TOKEN_CLAIM_NAME_APPID);
            Claim signClaim = map.get(TOKEN_CLAIM_NAME_SIGN);
            if(appIdClaim == null || signClaim == null) {
                if(log.isDebugEnabled()) {
                    log.debug("取token的appId,sign数据为空.Claims:{} ", map);
                }
                throw new RuntimeException("取token的appId数据为空");
            }
            if( ! appIdClaim.asString().equals(authVo.getAppId())) {
                if(log.isDebugEnabled()) {
                    log.debug("取token的appId数据不匹配,appIdClaim=auth.appId; {},{}", appIdClaim.asString(), authVo.getAppId());
                }
                throw new RuntimeException("取token的appId数据不匹配");
            }
            if( ! signClaim.asString().equals(authVo.getSign())) {
                if(log.isDebugEnabled()) {
                    log.debug("取token的sign数据不匹配,signClaim=auth.sign; {},{}", signClaim.asString(), authVo.getSign());
                }
                throw new RuntimeException("取token的sign数据不匹配");
            }
        }catch (TokenExpiredException e) {
            log.error("token已过期。", e);
            throw e;
        }catch (JWTVerificationException e) {
            log.error("token解析异常。", e);
            throw e;
        }catch (Exception e) {
            log.error("验证token失败。", e);
            throw new JWTVerificationException("验证token失败",e);
        }

    }

    /**
     * 刷新token,已过期的不能刷新
     *
     * @param token
     * @return
     */
    @Override
    public TokenResult.TokenData refreshToken(String token) {
        JwtKeyPair jwtKey = this.getTokenKey(); // 获取公钥/私钥
        try {
            // 其实按照规定只需要传递 publicKey 来校验即可
            Algorithm algorithm = this.generatorAlgorithm(jwtKey);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(JWT_ISSUER)
                    .build(); // Reusable verifier instance 可复用的验证实例
            DecodedJWT jwt = verifier.verify(token);
            Map<String, Claim> map = jwt.getClaims();

            Claim appIdClaim = map.get(TOKEN_CLAIM_NAME_APPID);
            Claim signClaim = map.get(TOKEN_CLAIM_NAME_SIGN);
            if(appIdClaim == null || signClaim == null) {
                if(log.isDebugEnabled()) {
                    log.debug("取token的appId,sign数据为空.Claims:{} ", map);
                }
                throw new RuntimeException("取token的appId数据为空,无法刷新");
            }
            AuthorizationVo authVo = new AuthorizationVo();
            authVo.setAppId(appIdClaim.asString());
            authVo.setSign(signClaim.asString());

            List<String> audienceLst = jwt.getAudience();
            return this.internalBuildToken(audienceLst.toArray(new String[audienceLst.size()]), authVo, null);
        }catch (TokenExpiredException e) {
            log.error("token已过期。", e);
            throw e;
        }catch (JWTVerificationException e) {
            log.error("token解析异常。", e);
            throw e;
        }catch (Exception e) {
            log.error("刷新token失败。", e);
            throw new JWTVerificationException("刷新token失败",e);
        }
    }

    /**
     * 记录在线token 到tokenOnlineCountMap
     *
     * @param token
     */
    @Override
    public void insertOlineToken(String token) {
        if (StringUtils.isBlank(token)) {
            return;
        }
        JwtKeyPair jwtKey = this.getTokenKey(); // 获取公钥/私钥
        try {
            // 其实按照规定只需要传递 publicKey 来校验即可
            Algorithm algorithm = this.generatorAlgorithm(jwtKey);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(JWT_ISSUER)
                    .build(); // Reusable verifier instance 可复用的验证实例
            DecodedJWT jwt = verifier.verify(token);

            //以签发时间为key
            Constants.tokenOnlineCountMap.put(jwt.getIssuedAt(), jwt.getExpiresAt());

        }catch (TokenExpiredException e) {
            log.error("token已过期。", e);
            throw e;
        }catch (JWTVerificationException e) {
            log.error("token解析异常。", e);
            throw e;
        }catch (Exception e) {
            log.error("记录在线token失败。", e);
            throw new JWTVerificationException("记录在线token失败",e);
        }
    }

}
